/*******************************************************************************
 * Copyright (c) 2011, 2012 Mia-Software.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Herve ESTEGUET (Mia-Software) - Bug 381352 - [Releng] There must be a project to configure the build by tycho
 *******************************************************************************/
package org.eclipse.releng.tycho.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Stack;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.DirSet;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.resources.FileResource;

public class CLOCTask extends Task {

	public static class Stat {
		public Stat(double scale) {
			this.scale = scale;
		}
		public int comments = 0;
		public int blank = 0;
		public int eloc = 0;
		public int totalLines = 0;
		public int files = 0;
		public final double scale;
	}

	private Stat javaStat = new Stat(1.36d);
	private Stat xmlStat = new Stat(1.9d);
	private Stat xsdStat = new Stat(1.9d);
	private Stat xslStat = new Stat(1.9d);
	private Stat dtdStat = new Stat(1.9d);
	private Stat htmlStat = new Stat(1.9d);
	private Stat dosStat = new Stat(0.63d);
	private Stat shStat = new Stat(0.63d);

	private DirSet dirset;
	private FileSet fileset;
	private File outputDir;
	private String outputFileName = "cloc.xml";

	@Override
	public void execute() throws BuildException {
		if (dirset == null && fileset == null) {
			throw new BuildException(String.format(
					"%s required a nested dirset or fileset elements", getTaskName()));
		}

		// We scan all the input
		if (dirset != null) {
			dirset.getDirectoryScanner().addDefaultExcludes();
			for (Iterator<?> it = dirset.iterator(); it.hasNext();) {
				FileResource dir = (FileResource) it.next();
				try {
					scanDirectory(dir.getFile());
				} catch(IOException e) {
					throw new BuildException("The metrics computation failed", e, getLocation());
				}
			}
		}

		if (fileset != null) {
			fileset.getDirectoryScanner().addDefaultExcludes();
			for (Iterator<?> it = fileset.iterator(); it.hasNext();) {
				FileResource file = (FileResource) it.next();
				try {
					scanFile(file.getFile());
				} catch(IOException e) {
					throw new BuildException("The metrics computation failed", e, getLocation());
				}
			}
		}

		//Creating the report file
		File resultFile;
		if (outputDir != null && outputDir.isDirectory()) {
			resultFile = new File(outputDir, outputFileName);
		} else {
			resultFile = new File(outputFileName);
			if (!resultFile.getParentFile().isDirectory()) {
				resultFile.getParentFile().mkdirs();
			}
		}
		if (!resultFile.getParentFile().isDirectory()) {
			throw new BuildException("Unable to compute the output directory", getLocation());
		}
		OutputStream output = null;
		try {
			output = new FileOutputStream(resultFile);
			log("Writing line counts to " + resultFile);
			buildReportFile(output);
		} catch (IOException e) {
			throw new BuildException("Unable to save metrics to " + resultFile.getAbsolutePath(), e, getLocation()); 
		} finally {
			try {
				output.close();
			} catch(Exception e) {
				//Do nothing
			}
		}
	}

	public void setOutputDir(File anOutputDir) {
		this.outputDir = anOutputDir;
	}

	public void setOutputFileName(String aFileName) {
		if (aFileName != null) {
			this.outputFileName = aFileName;
		}
	}

	public void addConfiguredDirset(DirSet aDirSet) {
		this.dirset = aDirSet;
	}

	public void addConfiguredFileset(FileSet aFileSet) {
		this.fileset = aFileSet;
	}

	public void scanDirectory(File dir) throws IOException {
		Stack<File> scanStack = new Stack<File>();
		scanStack.push(dir);
		while (!scanStack.empty()) {
			File pwd = scanStack.pop();
			for (File file : pwd.listFiles()) {
				if (file.isDirectory()) {
					scanStack.push(file);
				} else {
					scanFile(file);
				}
			}
		}
	}

	public void scanFile(File file) throws IOException {
		if (file.getName().endsWith(".java")) {
			log("Scanning " + file);
			scanJava(file);
		} else if (file.getName().endsWith(".xml")) {
			log("Scanning " + file);
			scanXML(file, xmlStat);
		} else if(file.getName().endsWith(".xsl") || file.getName().endsWith(".xslt")) {
			log("Scanning " + file);
			scanXML(file, xslStat);
		}  else if(file.getName().endsWith(".html") 
				|| file.getName().endsWith("htm") 
				|| file.getName().endsWith("xhtml")) {
			log("Scanning " + file);
			scanXML(file, htmlStat);
		} else if(file.getName().endsWith(".xsd")) {
			log("Scanning " + file);
			scanXML(file, xsdStat);
		}  else if(file.getName().endsWith(".dtd")) {
			log("Scanning " + file);
			scanXML(file, dtdStat);
		} else if(file.getName().endsWith(".sh")) {
			log("Scanning " + file);
			scanUnixShell(file, shStat);
		}  else if(file.getName().endsWith(".bat")) {
			log("Scanning " + file);
			scanDosBatch(file, dosStat);
		} else {
			log("Unsupported file " + file);
		}
	}

	public void scanJava(File file) throws IOException {
		Scanner input = new Scanner(file);
		try {
			String line;
			javaStat.files++;
			while (input.hasNext()) {
				line = input.nextLine().trim();
				javaStat.eloc++;
				javaStat.totalLines++;
				if (line.length() == 0) {
					javaStat.blank++;
				}

				// Single line comment
				else if (line.startsWith("//")) {
					javaStat.comments++;
				} else if (line.matches("(^(//))//.*")) {
					// This could fails for code like (String s =
					// "// Hello world")
					javaStat.comments++;
					javaStat.eloc++;
				}

				// Multiline comment
				else if (line.contains("/*")) {
					javaStat.comments++;
					// Case like : */ /*
					// : "/*"
					// Wont work
					while (!line.contains("*/") && input.hasNext()) {
						javaStat.comments++;
						javaStat.eloc++;
						line = input.nextLine();
						javaStat.totalLines++;
					}
				}

				// Executable line
				else {
					javaStat.eloc++;
				}
			}
		} finally {
			input.close();
		}
	}

	public void scanXML(File file, Stat stat) throws IOException {
		Scanner input = new Scanner(file);
		try {
			String line;
			stat.files++;
			while (input.hasNext()) {
				line = input.nextLine().trim();
				stat.totalLines++;
				// Blank
				if (line.length() == 0) {
					stat.blank++;
				}

				// comment
				else if (line.startsWith("<!--")) {
					stat.comments++;
					if (!line.contains("-->")) {
						if (line.endsWith("-->")) {
							stat.eloc++;
						}
					} else if (input.hasNext()) {
						do {
							line = input.nextLine();
							stat.totalLines++;
							stat.comments++;
						} while (!line.contains("-->") && input.hasNext());
						if (!line.contains("-->")) {
							if (line.endsWith("-->")) {
								stat.eloc++;
							}
						}
					}
				}

				// executable lines
				else {
					stat.eloc++;
				}
			}
		} finally {
			input.close();
		}
	}

	public void scanSingleLineCommentLanguage(File file, Stat stat, boolean lineStart, String... commentDelimiters) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
		try {
			String line = null;
			while ((line = reader.readLine()) != null) {
				line = line.trim();
				if (line.length() == 0) {
					stat.blank++;
				} else {
					String delim = null;
					for (String aDelim : commentDelimiters) {
						if (line.contains(aDelim)) {
							delim = aDelim;
							break;
						}
					}
					if (delim != null) {				
						if (!line.startsWith(delim)) {
							if (!lineStart) {
								stat.comments++;
							}
							stat.eloc++;
						} else {
							stat.comments++;
						}
					}
					else {
						stat.eloc++;
					}
				}
				stat.totalLines++;
			}
		} finally {
			reader.close();
		}
	}

	public void scanUnixShell(File file, Stat stat) throws IOException {
		//The first line of a shell should be #!/bin/*sh
		BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
		try {
			String line = reader.readLine();
			if (line != null && !line.trim().matches("#!/bin/[\\w]*sh")) {
				return;
			}
		} finally {		
			reader.close();
		}
		//We scan
		this.scanSingleLineCommentLanguage(file, stat, false, "#");
	}

	public void scanDosBatch(File file, Stat stat) throws IOException {
		this.scanSingleLineCommentLanguage(file, stat, true, "REM", "::");
	}

	public void buildReportFile(OutputStream ostream) {
		PrintStream out = new PrintStream(ostream);
		out.println("<?xml version=\"1.0\" ?>");
		out.println("<languages>");
		writeStat(out, "Java", javaStat);
		if (xmlStat.totalLines > 0)
			writeStat(out, "XML", xmlStat);
		if (xsdStat.totalLines > 0)
			writeStat(out, "XSD", xsdStat);
		if (xslStat.totalLines > 0)
			writeStat(out, "XSL", xslStat);
		if (htmlStat.totalLines > 0)
			writeStat(out, "HTML", htmlStat);
		if (dtdStat.totalLines > 0)
			writeStat(out, "DTD", dtdStat);
		if (dosStat.totalLines > 0)
			writeStat(out, "DOS Batch", dosStat);
		if (shStat.totalLines > 0)
			writeStat(out, "UNIX Shell", shStat);
		out.println("</languages>");
	}

	private void writeStat(PrintStream out, String name, Stat stat) {
		out.printf("<language name=\"%s\" files_count=\"%d\" blank=\"%d\" comment=\"%d\" code=\"%d\" factor=\"%.2f\" scaled=\"%.2f\"/>"
				, name, stat.totalLines, stat.blank, stat.comments, stat.eloc, stat.scale, stat.scale * stat.eloc);
	}

}
